package com.example.springdemo.filter;

import com.example.springdemo.config.Swagger3;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Data;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.Ordered;
import org.springframework.http.HttpMethod;
import org.springframework.scheduling.annotation.Async;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;
import org.springframework.web.util.WebUtils;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.Objects;

/**
 * HttpTraceLogFilter
 *
 * @author wenguang
 * @date 2020/12/14 16:33
 */
@Slf4j
public class HttpTraceLogFilter extends OncePerRequestFilter implements Ordered {

    private static final String NEED_TRACE_PATH_PREFIX = "/";

    private static final String IGNORE_CONTENT_TYPE = "multipart/form-data";

    private static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();

    @Override
    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE - 10;
    }

    @Override
    @Async("asyncTaskExecutor")
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
        throws ServletException, IOException {
        if (!isRequestValid(request) ||
            filterUrl(request)) {
            filterChain.doFilter(request, response);
            return;
        }

        if (!(request instanceof ContentCachingRequestWrapper)) {
            request = new ContentCachingRequestWrapper(request);
        }
        if (!(response instanceof ContentCachingResponseWrapper)) {
            response = new ContentCachingResponseWrapper(response);
        }

        LocalDateTime start = LocalDateTime.now();
        try {
            filterChain.doFilter(request, response);
        } finally {
            String path = request.getRequestURI();
            if (path.startsWith(NEED_TRACE_PATH_PREFIX)
                && !Objects.equals(IGNORE_CONTENT_TYPE, request.getContentType())) {
                logTrace(request, response, start);
            }
            updateResponse(response);
        }
    }

    @Async("asyncTaskExecutor")
    public void logTrace(HttpServletRequest request, HttpServletResponse response, LocalDateTime start) throws JsonProcessingException {
        String requestBody = getRequestBody(request);
        String responseBody = new String(((ContentCachingResponseWrapper) response).getContentAsByteArray());
        long latency =
            LocalDateTime.now().toInstant(ZoneOffset.of("+8")).toEpochMilli() - start.toInstant(ZoneOffset.of("+8")).toEpochMilli();

        String name = Thread.currentThread().getName();

        TranceLogBean bean = new TranceLogBean();
        bean.setTime(LocalDateTime.now())
            .setMethod(request.getMethod())
            .setStatus(response.getStatus())
            .setUrl(request.getRequestURL().toString())
            .setRequestBody(requestBody)
            .setResponseBody(responseBody)
            .setTimeTaken(latency);

        log.info("{}", bean);
    }

    private String getRequestBody(HttpServletRequest request) throws JsonProcessingException {
        ContentCachingRequestWrapper wrapper = WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class);
        return getRequestString(wrapper);
    }

    private String getRequestString(ContentCachingRequestWrapper request) throws JsonProcessingException {
        String payload = "{}";
        if (request != null) {
            if (request.getMethod().equalsIgnoreCase(HttpMethod.POST.name())) {
                byte[] buf = request.getContentAsByteArray();
                if (buf.length > 0 && buf.length < 1024 * 64) {
                    try {
                        payload = new String(buf, 0, buf.length, request.getCharacterEncoding());
                    } catch (UnsupportedEncodingException ex) {
                        payload = "[unknown]";
                    }
                } else if (buf.length >= 1024 * 64) {
                    return String.format("[too long content, length = %s]", buf.length);
                }
            } else {
                ObjectMapper mapper = new ObjectMapper();
                payload = mapper.writeValueAsString(request.getParameterMap());
            }
        }
        return payload;
    }

    private boolean isRequestValid(HttpServletRequest request) {
        try {
            new URI(request.getRequestURL().toString());
            return true;
        } catch (URISyntaxException ex) {
            return false;
        }
    }

    private boolean filterUrl(HttpServletRequest request) {
        String uri = request.getRequestURI();

        return Arrays.stream(Swagger3.URLS)
            .anyMatch(x -> PATH_MATCHER.match(x, uri));
    }

    private void updateResponse(HttpServletResponse response) throws IOException {
        ContentCachingResponseWrapper responseWrapper =
            WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);
        Objects.requireNonNull(responseWrapper).copyBodyToResponse();
    }

    @Data
    @Accessors(chain = true)
    private static class TranceLogBean {

        /**
         * 时间格式化器
         */
        public static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss SSS");

        /**
         * 请求时间
         */
        @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
        private LocalDateTime time;

        /**
         * 请求方法
         */
        private String method;

        /**
         * 响应状态
         */
        private int status;

        /**
         * 请求url
         */
        private String url;

        /**
         * 请求体
         */
        private String requestBody;

        /**
         * 响应体
         */
        private String responseBody;

        /**
         * 耗时
         */
        private long timeTaken;

        @Override
        public String toString() {
            return "===================================================\r\n" +
                "time : " + time.format(FORMATTER) + ",\r\n" +
                "method : " + method + ",\r\n" +
                "status : " + status + ",\r\n" +
                "url : " + url + ",\r\n" +
                "requestBody : " + requestBody + ",\r\n" +
                "responseBody : " + responseBody + ",\r\n" +
                "timeTaken : " + timeTaken + "ms\r\n" +
                "===================================================\r\n";
        }

    }

}
